<html>
<head>
<style>
body {
  font-family: Verdana, Arial;
  font-size: 12px;
}

/* Give the same font styling to form elements. */
input, select, textarea, button {
  font-family: inherit;
  font-size: inherit;
}

.content {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

.description {
  padding-bottom: 5px;
}

.description .title {
  font-size: 120%;
  font-weight: bold;
}

.route_controls {
  flex: 0;
  align-self: center;
  text-align: right;
  padding-bottom: 5px;
}

.route_controls .label {
  display: inline-block;
  vertical-align: top;
  font-weight: bold;
}

.route_controls .control {
  width: 500px;
}

.messages {
  flex: 1;
  min-height: 100px;
  border: 1px solid gray;
  overflow: auto;
}

.messages .message {
  padding: 3px;
  border-bottom: 1px solid #cccbca;
}

.messages .message .timestamp {
  font-size: 90%;
  font-style: italic;
}

.messages .status {
  background-color: #d6d6d6;  /* light gray */
}

.messages .sent {
  background-color: #c5e8fc;  /* light blue */
}

.messages .recv {
  background-color: #fcf4e3;  /* light yellow */
}

.message_controls {
  flex: 0;
  text-align: right;
  padding-top: 5px;
}

.message_controls textarea {
  width: 100%;
  height: 10em;
}
</style>
<script language="JavaScript">
// Application state.
var demoMode = false;
var currentSubscriptionId = null;
var currentRouteId = null;

// List of currently supported source protocols.
var allowedSourceProtocols = ['cast', 'dial'];

// Values from cef_media_route_connection_state_t.
var CEF_MRCS_UNKNOWN = 0;
var CEF_MRCS_CONNECTING = 1;
var CEF_MRCS_CONNECTED = 2;
var CEF_MRCS_CLOSED = 3;
var CEF_MRCS_TERMINATED = 4;

function getStateLabel(state) {
  switch (state) {
    case CEF_MRCS_CONNECTING: return "CONNECTING";
    case CEF_MRCS_CONNECTED: return "CONNECTED";
    case CEF_MRCS_CLOSED: return "CLOSED";
    case CEF_MRCS_TERMINATED: return "TERMINATED";
    default: break;
  }
  return "UNKNOWN";
}

// Values from cef_media_sink_icon_type_t.
var CEF_MSIT_CAST = 0;
var CEF_MSIT_CAST_AUDIO_GROUP = 1;
var CEF_MSIT_CAST_AUDIO = 2;
var CEF_MSIT_MEETING = 3;
var CEF_MSIT_HANGOUT = 4;
var CEF_MSIT_EDUCATION = 5;
var CEF_MSIT_WIRED_DISPLAY = 6;
var CEF_MSIT_GENERIC = 7;

function getIconTypeLabel(type) {
  switch (type) {
    case CEF_MSIT_CAST: return "CAST";
    case CEF_MSIT_CAST_AUDIO_GROUP: return "CAST_AUDIO_GROUP";
    case CEF_MSIT_CAST_AUDIO: return "CAST_AUDIO";
    case CEF_MSIT_MEETING: return "MEETING";
    case CEF_MSIT_HANGOUT: return "HANGOUT";
    case CEF_MSIT_EDUCATION: return "EDUCATION";
    case CEF_MSIT_WIRED_DISPLAY: return "WIRED_DISPLAY";
    case CEF_MSIT_GENERIC: return "GENERIC";
    default: break;
  }
  return "UNKNOWN";
}


///
// Manage show/hide of default text for form elements.
///

// Default messages that are shown until the user focuses on the input field.
var defaultSourceText = 'Enter URN here and click "Create Route"';
var defaultMessageText = 'Enter message contents here and click "Send Message"';

function getDefaultText(control) {
  if (control === 'source')
    return defaultSourceText;
  if (control === 'message')
    return defaultMessageText;
  return null;
}

function hideDefaultText(control) {
  var element = document.getElementById(control);
  var defaultText = getDefaultText(control);
  if (element.value === defaultText)
    element.value = '';
}

function showDefaultText(control) {
  var element = document.getElementById(control);
  var defaultText = getDefaultText(control);
  if (element.value === '')
    element.value = defaultText;
}

function initDefaultText() {
  showDefaultText('source');
  showDefaultText('message');
}


///
// Retrieve current form values. Return null if validation fails.
///

function getCurrentSource() {
  var sourceInput = document.getElementById('source');
  var value = sourceInput.value;
  if (value === defaultSourceText || value.length === 0 || value.indexOf(':') < 0) {
    return null;
  }

  // Validate the URN value.
  try {
    var url = new URL(value);
    if ((url.hostname.length === 0 && url.pathname.length === 0) ||
        !allowedSourceProtocols.includes(url.protocol.slice(0, -1))) {
      return null;
    }
  } catch (e) {
    return null;
  }

  return value;
}

function getCurrentSink() {
  var sinksSelect = document.getElementById('sink');
  if (sinksSelect.options.length === 0)
    return null;
  return sinksSelect.value;
}

function getCurrentMessage() {
  var messageInput = document.getElementById('message');
  if (messageInput.value === defaultMessageText || messageInput.value.length === 0)
    return null;
  return messageInput.value;
}


///
// Set disabled state of form elements.
///

function updateControls() {
  document.getElementById('source').disabled = hasRoute();
  document.getElementById('sink').disabled = hasRoute();
  document.getElementById('create_route').disabled =
      hasRoute() || getCurrentSource() === null || getCurrentSink() === null;
  document.getElementById('terminate_route').disabled = !hasRoute();
  document.getElementById('message').disabled = !hasRoute();
  document.getElementById('send_message').disabled = !hasRoute() || getCurrentMessage() === null;
}


///
// Manage the media sinks list.
///

/*
Expected format for |sinks| is:
  [
    {
      name: string,
      type: string ('cast' or 'dial'),
      id: string,
      desc: string,
      icon: int
    }, ...
  ]
*/
function updateSinks(sinks) {
  var sinksSelect = document.getElementById('sink');

  // Currently selected value.
  var selectedValue = sinksSelect.options.length === 0 ? null : sinksSelect.value;

  // Build a list of old (existing) values.
  var oldValues = [];
  for (var i = 0; i < sinksSelect.options.length; ++i) {
    oldValues.push(sinksSelect.options[i].value);
  }

  // Build a list of new (possibly new or existing) values.
  var newValues = [];
  for(var i = 0; i < sinks.length; i++) {
    newValues.push(sinks[i].id);
  }

  // Remove old values that no longer exist.
  for (var i = sinksSelect.options.length - 1; i >= 0; --i) {
    if (!newValues.includes(sinksSelect.options[i].value)) {
      sinksSelect.remove(i);
    }
  }

  // Add new values that don't already exist.
  for(var i = 0; i < sinks.length; i++) {
    var sink = sinks[i];
    if (oldValues.includes(sink.id))
      continue;
    var opt = document.createElement('option');
    opt.innerHTML = sink.name + ' (' + sink.model_name + ', ' + sink.type + ', ' +
                    getIconTypeLabel(sink.icon) + ', ' + sink.ip_address + ':' + sink.port + ')';
    opt.value = sink.id;
    sinksSelect.appendChild(opt);
  }

  if (sinksSelect.options.length === 0) {
    selectedValue = null;
  } else if (!newValues.includes(selectedValue)) {
    // The previously selected value no longer exists.
    // Select the first value in the new list.
    selectedValue = sinksSelect.options[0].value;
    sinksSelect.value = selectedValue;
  }

  updateControls();

  return selectedValue;
}


///
// Manage the current media route.
///

function hasRoute() {
  return currentRouteId !== null;
}

function createRoute() {
  console.assert(!hasRoute());
  var source = getCurrentSource();
  console.assert(source !== null);
  var sink = getCurrentSink();
  console.assert(sink !== null);

  if (demoMode) {
    onRouteCreated('demo-route-id');
    return;
  }

  sendCefQuery(
    {name: 'createRoute', source_urn: source, sink_id: sink},
    (message) => onRouteCreated(JSON.parse(message).route_id)
  );
}

function onRouteCreated(route_id) {
  currentRouteId = route_id;
  showStatusMessage('Route ' + route_id + '\ncreated');
  updateControls();
}

function terminateRoute() {
  console.assert(hasRoute());
  var source = getCurrentSource();
  console.assert(source !== null);
  var sink = getCurrentSink();
  console.assert(sink !== null);

  if (demoMode) {
    onRouteTerminated();
    return;
  }

  sendCefQuery(
    {name: 'terminateRoute', route_id: currentRouteId},
    (unused) => {}
  );
}

function onRouteTerminated() {
  showStatusMessage('Route ' + currentRouteId + '\nterminated');
  currentRouteId = null;
  updateControls();
}


///
// Manage messages.
///

function sendMessage() {
  console.assert(hasRoute());
  var message = getCurrentMessage();
  console.assert(message !== null);

  if (demoMode) {
    showSentMessage(message);
    setTimeout(function(){ if (hasRoute()) { recvMessage('Demo ACK for: ' + message); } }, 1000);
    return;
  }

  sendCefQuery(
    {name: 'sendMessage', route_id: currentRouteId, message: message},
    (unused) => showSentMessage(message)
  );
}

function recvMessage(message) {
  console.assert(hasRoute());
  console.assert(message !== undefined && message !== null && message.length > 0);
  showRecvMessage(message);
}

function showStatusMessage(message) {
  showMessage('status', message);
}

function showSentMessage(message) {
  showMessage('sent', message);
}

function showRecvMessage(message) {
  showMessage('recv', message);
}

function showMessage(type, message) {
  if (!['status', 'sent', 'recv'].includes(type)) {
    console.warn('Invalid message type: ' + type);
    return;
  }

  if (message[0] === '{') {
    try {
      // Pretty print JSON strings.
      message = JSON.stringify(JSON.parse(message), null, 2);
    } catch(e) {}
  }

  var messagesDiv = document.getElementById('messages');

  var newDiv = document.createElement("div");
  newDiv.innerHTML =
      '<span class="timestamp">' + (new Date().toLocaleString()) +
      ' (' + type.toUpperCase() + ')</span><br/>';
  // Escape any HTML tags or entities in |message|.
  var pre = document.createElement('pre');
  pre.appendChild(document.createTextNode(message));
  newDiv.appendChild(pre);
  newDiv.className = 'message ' + type;

  messagesDiv.appendChild(newDiv);

  // Always scroll to bottom.
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
}


///
// Manage communication with native code in media_router_test.cc.
///

function onCefError(code, message) {
  showStatusMessage('ERROR: ' + message + ' (' + code + ')');
}

function sendCefQuery(payload, onSuccess, onFailure=onCefError, persistent=false) {
  // Results in a call to the OnQuery method in media_router_test.cc
  return window.cefQuery({
    request: JSON.stringify(payload),
    onSuccess: onSuccess,
    onFailure: onFailure,
    persistent: persistent
  });
}

/*
Expected format for |message| is:
  {
    name: string,
    payload: dictionary
  }
*/
function onCefSubscriptionMessage(message) {
  if (message.name === 'onSinks') {
    // List of sinks.
    updateSinks(message.payload.sinks_list);
  } else if (message.name === 'onRouteStateChanged') {
    // Route status changed.
    if (message.payload.route_id === currentRouteId) {
      var connection_state = message.payload.connection_state;
      showStatusMessage('Route ' + currentRouteId +
                        '\nconnection state ' + getStateLabel(connection_state) +
                        ' (' + connection_state + ')');
      if ([CEF_MRCS_CLOSED, CEF_MRCS_TERMINATED].includes(connection_state)) {
        onRouteTerminated();
      }
    }
  } else if (message.name === 'onRouteMessageReceived') {
    // Route message received.
    if (message.payload.route_id === currentRouteId) {
      recvMessage(message.payload.message);
    }
  }
}

// Subscribe to ongoing message notifications from the native code.
function startCefSubscription() {
  currentSubscriptionId = sendCefQuery(
    {name: 'subscribe'},
    (message) => onCefSubscriptionMessage(JSON.parse(message)),
    (code, message) => {
      onCefError(code, message);
      currentSubscriptionId = null;
    },
    true
  );
}

function stopCefSubscription() {
  if (currentSubscriptionId !== null) {
    // Results in a call to the OnQueryCanceled method in media_router_test.cc
    window.cefQueryCancel(currentSubscriptionId);
  }
}


///
// Example app load/unload.
///

function initDemoMode() {
  demoMode = true;

  var sinks = [
    {
      name: 'Sink 1',
      type: 'cast',
      id: 'sink1',
      desc: 'My cast device',
      icon: CEF_MSIT_CAST
    },
    {
      name: 'Sink 2',
      type: 'dial',
      id: 'sink2',
      desc: 'My dial device',
      icon: CEF_MSIT_GENERIC
    }
  ];
  updateSinks(sinks);

  showStatusMessage('Running in Demo mode.');
  showSentMessage('Demo sent message.');
  showRecvMessage('Demo recv message.');
}

function onLoad() {
  initDefaultText();

  if (window.cefQuery === undefined) {
    // Initialize demo mode when running outside of CEF.
    // This supports development and testing of the HTML/JS behavior outside
    // of a cefclient build.
    initDemoMode();
    return;
  }

  startCefSubscription()
}

function onUnload() {
  if (demoMode)
    return;

  if (hasRoute())
    terminateRoute();
  stopCefSubscription();
}
</script>
<title>Media Router Example</title>
</head>
<body bgcolor="white" onLoad="onLoad()" onUnload="onUnload()">
<div class="content">
  <div class="description">
    <span class="title">Media Router Example</span>
    <p>
      <b>Overview:</b>
      Chromium supports communication with devices on the local network via the
      <a href="https://blog.oakbits.com/google-cast-protocol-overview.html" target="_blank">Cast</a> and
      <a href="http://www.dial-multiscreen.org/" target="_blank">DIAL</a> protocols.
      CEF exposes this functionality via the CefMediaRouter interface which is demonstrated by this test.
      Test code is implemented in resources/media_router.html and browser/media_router_test.cc.
    </p>
    <p>
      <b>Usage:</b>
      Devices available on your local network will be discovered automatically and populated in the "Sink" list.
      Enter a URN for "Source", select an available device from the "Sink" list, and click the "Create Route" button.
      Cast URNs take the form "cast:<i>&lt;appId&gt;</i>?clientId=<i>&lt;clientId&gt;</i>" and DIAL URNs take the form "dial:<i>&lt;appId&gt;</i>",
      where <i>&lt;appId&gt;</i> is the <a href="https://developers.google.com/cast/docs/registration" target="_blank">registered application ID</a>
      and <i>&lt;clientId&gt;</i> is an arbitrary numeric identifier.
      Status information and messages will be displayed in the center of the screen.
      After creating a route you can send messages to the receiver app using the textarea at the bottom of the screen.
      Messages are usually in JSON format with a example of Cast communication to be found
      <a href="https://bitbucket.org/chromiumembedded/cef/issues/2900/add-mediarouter-support-for-cast-receiver#comment-56680326" target="_blank">here</a>.
    </p>
  </div>
  <div class="route_controls">
    <span class="label">Source:</span>
    <input type="text" id="source" class="control" onInput="updateControls()" onFocus="hideDefaultText('source')" onBlur="showDefaultText('source')"/>
    <br/>
    <span class="label">Sink:</span>
    <select id="sink" size="3" class="control"></select>
    <br/>
    <input type="button" id="create_route" onclick="createRoute()" value="Create Route" disabled/>
    <input type="button" id="terminate_route" onclick="terminateRoute()" value="Terminate Route" disabled/>
  </div>
  <div id="messages" class="messages">
  </div>
  <div class="message_controls">
    <textarea id="message" onInput="updateControls()" onFocus="hideDefaultText('message')" onBlur="showDefaultText('message')" disabled></textarea>
    <br/><input type="button" id="send_message" onclick="sendMessage()" value="Send Message" disabled/>
  </div>
</div>
</body>
</html>
